package com.example.springsecurityjwt.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.Date;

/**
 * @author abbys
 *
 *
 *（***）用户登录的过滤器，在用户的登录的过滤器中校验用户是否登录成功，如果登录成功，则生成一个token返回给客户端，登录失败则给前端一个登录失败的提示。
 *
 * 步骤:
 * 1.attemptAuthentication方法中，我们从登录参数中提取出用户名密码，然后调用AuthenticationManager.authenticate()方法去进行自动校验。
 * AuthenticationManager.authenticate()就是调用springsecurity的UserDetailService的loadUserByName方法,这个方法一般我们就自己继承
 * 接口然后实现写自己的方法逻辑,其实就是查询数据库有无这个用户,有就是校验成功,没有就是校验失败.
 *
 * 2.如果校验成功，就会来到successfulAuthentication回调中，在successfulAuthentication方法中，
 *   将用户角色遍历然后用一个 , 连接起来，然后再利用Jwts去生成token，按照代码的顺序，生成过程一共配置了四个参数
 *   分别是 用户角色、主题、过期时间以及加密算法和密钥，然后将生成的token写出到客户端。
 *  如果校验失败就会来到unsuccessfulAuthentication方法中，在这个方法中返回一个错误提示给客户端即可。
 *
 *
 * (***) springsecurity大致核心流程
 * 1.用户输入账号密码以后先会走的这个过滤器中的attemptAuthentication方法，这个方法是用来校验前端传递过来的用户名或密码的
 * 例如前端会把传递过来的账号密码加密，所以这里也可以进行密码解密，这里是不会去数据库查询看有没有这个用户的，这里只是对用户传递的用户名密码进行校验或其他处理
 * 且spirngsecurity登录默认是使用key/value的形式 所以在attemptAuthentication方法中我们得使用原生request对象获得传递过来的用户名和密码
 * 我们可以想办法改造为接收json形式
 *
 * 2.当第一步完成后才会走UserDetailService的loadUserByName方法，该方法一般都是我们继承UserDetailsService接口自己实现的
 * 这个方法的逻辑就是去数据库查询是否有这个用户,当然如果我们不连接数据库而是在内存中生成用户的话在springsecurity的配置类中进行配置就行
 * 然后当检测到数据库中有这个用户的时候就代表成功了就调用successfulAuthentication方法，如果失败调用unsuccessfulAuthentication
 *
 * 方法执行顺序  attemptAuthentication  --> loadUserByName/在内存中找用户且认证  --> successfulAuthentication(认证成功调用)/unsuccessfulAuthentication(认证失败调用)
 *
 *  因为继承了UsernamePasswordAuthenticationFilter,所以每次只会在登陆的时候走这个过滤器
 *
 */
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;

    public JwtLoginFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse resp) throws AuthenticationException {

        /**
         *  这里参数提交方式是key/value形式是springsecurity默认的 即前端请求时参数是写在params中而不是json格式
         *  localhost:8080/login?username=admin&password=123
         *
         *  因为整合了springsecurity而且我们没修改,所以默认用户名跟密码前端都必须传 username和password
         *
         */
        String username = this.obtainUsername(req);
        String password = this.obtainPassword(req);

        System.out.println("用户传递的登录名:"+username);
        System.out.println("用户传递的密码:"+password);

        //怎么改成让参数提交方式为json形式呢  https://blog.csdn.net/u012702547/article/details/105215887


        /**
         * springsecurity去帮我们校验是否通过认证 (验证用户是否登录成功)
         * 其实主要是两个用户数据获取和一个比较。
         * 第一个用户数据获取：从浏览器输入的用户和密码，用户名和密码会传递到Spring Security内部。
         * 第二个用户数据获取：根据用户名从数据库获得用户详情或者从内存中我们写死的获得用户详情。
         * 一个比较：将前面两个数据获取进行比较，匹配则通过验证，否则验证不通过。
         *
         * 所以也就解释了为什么loadUserByName(数据库用户方式)方法只有一个username参数了
         *
         */
        return authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(username, password));
    }


    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse resp, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        //因为该方法是在loadUserByname或者内存中查询完用户之后才调用，所以authResult.getAuthorities();是返回用户的角色
        Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities();

        System.out.println("角色:"+authorities); //用admin登录这里就是 角色:[ROLE_admin] 用user登录这里就是[ROLE_user]

        StringBuffer as = new StringBuffer();
        for (GrantedAuthority authority : authorities) {
            as.append(authority.getAuthority())
                    .append(",");
        }

        String jwt = Jwts.builder()
                .claim("authorities", as)   //配置用户角色
                .setSubject(authResult.getName())  //登录名
                .setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000))  //过期时间
                .signWith(SignatureAlgorithm.HS512,"sang@123")  //加密算法HS512 密钥sang@123
                .compact();

        System.out.println("生成的token:"+jwt);

        resp.setContentType("application/json;charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.write(new ObjectMapper().writeValueAsString(jwt));
        out.flush();
        out.close();
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse resp, AuthenticationException failed) throws IOException, ServletException {
        resp.setContentType("application/json;charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.write("登录失败!");
        out.flush();
        out.close();
    }
}